import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Queue;
import java.util.Random;
import java.util.Stack;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;

import main.SimpleRTTClient;
import main.SimpleRTTServer;

public class Bootstrap {
	// Contains list of children once connected to tree
	private Vector<String> childNodeList;
	// Contains address of root Node of tree
	private String rootNode;
	// Contains address of parent node in tree
	private String parentNode;
	// Contains address of potential parent node in tree
	private String potentialParent;
	// Contains address of nodes on the path from root to this client
	private String[] rootPath;
	//private Stack<String> rootPath;
	// Contains this client's Address 
	protected String myAddress;
	// Current minimum RTT time encountered in join phase
	private double minRTT;
	// Contains Rendezvous Point Address, passed from command line to Client class.
	private String RPAddress;
	// Underlying socket communication 
	private SocketComm comm;
	// RTT client which Chris made keeping it separated
	private SimpleRTTClient rttClient;
	private SimpleRTTServer rttServer;
	
	private Timer refreshTimer;
	private static int delay = 0;
	private static int refreshPeriod = 5000;
	private static int timerRefreshSent = 0;
	private static int randomIntRange = 4000;
	private Timer pathImprovementTimer;
	private static int improvementPeriod = 30000;
	
	// flag variables
	private boolean isJoining;
	private boolean isLeaving;	
	
	
	// Stuff maybe used for packets sent
	// Please note the explicit use of spaces in some strings while not in others
	private static final String REQUESTCHILDLIST = "REQUESTCHILDREN ";
	private static final String CHILDLIST = "CHILDLIST";
	private static final String REQUESTJOIN = "JOIN ";
	private static final String ACKJOIN = "ACKJOIN ";
	private static final String NOTIFYLEAVE = "LEAVE ";
	private static final String REFRESHREQUEST = "REFRESH ";
	private static final String NOTIFYPATH = "PATH";
	private static final String DATASENT = "DATA ";
	
	public class RefreshThread extends TimerTask {
		@Override
		public void run() {
			System.out.println("Sending REFRESH packet to Parent: " +parentNode);
			if(timerRefreshSent > 3) {	// We have not gotten a PATH reply, so assume parent crashed
				System.out.println("Haven't heard back from Parent, rejoining: ");
				parentNode = null;
				try {
					this.cancel();
					startJoinTreeRepair();
					
				} catch (UnknownHostException e) {
					System.err.println("Node specified for rejoining is UnknownHost.");
					e.printStackTrace();
				} catch (InterruptedException e) {
					System.err.println("Timer was Interrupted.");
					e.printStackTrace();
				}
				//TODO: need to set a timer in case we try to contact grandparent node and never get 
				// a response
				
			}
			else{
				try {
					comm.sendDatagram(InetAddress.getByName(parentNode), "REFRESH");
				} catch (UnknownHostException e) {
					System.err.println("Node specified for rejoining is UnknownHost.");
					e.printStackTrace();
				}
				timerRefreshSent++;
			}
		}
	}
	
	public class ImprovementThread extends TimerTask {
		@Override
		public void run() {
			System.out.println("Improvement Timer expired, attempting rejoin. ");
			try {
				// If you're not the root node, then try to improve 
				if(!rootNode.equals(myAddress)){
					// Stop REFRESH messages from being sent
					refreshTimer.cancel();
					// Let your parent know you're leaving the group 
					if(parentNode != null || !parentNode.equals("")){
						comm.sendDatagram(InetAddress.getByName(parentNode), NOTIFYLEAVE);
					}
					// Let your children know you're leaving your group
					if(childNodeList.size() > 0){
						for(String child : childNodeList){
							comm.sendDatagram(InetAddress.getByName(child), NOTIFYLEAVE);
						}
					}					
					int rand = new Random(System.currentTimeMillis()).nextInt(rootPath.length);
					isJoining = true;
					System.out.println("Joining Algorithm started - Requesting child list from: " + rootPath[rand]);
					comm.sendDatagram(InetAddress.getByName(rootPath[rand]), REQUESTCHILDLIST);
					this.cancel();
				}
			} catch (UnknownHostException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	public static void main(String[] args){
		Bootstrap boot = new Bootstrap("127.0.0.1");
	}
	
	// Constructor
	public Bootstrap(String RPAdd){
		try{
			// Attempt to get local IP address, not sure if this will return 127.0.0.1
			childNodeList = new Vector<String>();
			myAddress = InetAddress.getLocalHost().getHostAddress();
			System.out.println("Selecting " + myAddress + " as myAddress.");
			InetAddress[] addys = InetAddress.getAllByName(InetAddress.getLocalHost().getHostName());
			for (InetAddress a : addys){
				System.out.println("Other possible address chocies include: " + a.getHostAddress());
				if(a.getHostAddress().startsWith("192.168.0.")){
					System.out.println("Selecting " + a.getHostAddress() + " as myAddress.");
					myAddress = a.getHostAddress();
				}
			}
			
			//System.out.println("Local Host Address info:" + InetAddress.getLocalHost().toString());
			parentNode = "";
			RPAddress = RPAdd;
			isJoining = false;
			isLeaving = false;
			minRTT = 1029438923;
			System.out.println("Starting UDP socket communication.");
			comm = new SocketComm(this);			
			System.out.println("Starting RTT Server.");
			rttServer = new SimpleRTTServer();
			System.out.println("Starting RTT Client.");
			rttClient = new SimpleRTTClient();
			System.out.println("Initilizing REFRESH timer.");
			refreshTimer = new Timer();
			pathImprovementTimer = new Timer();
			System.out.println("Acquiring Root address from RP.");
			contactRP();
			System.out.println("Starting Join.");
			startJoinTree();
		}catch(UnknownHostException e){
			e.printStackTrace();
		} catch (IOException e) {
			System.err.println("Could not start RTT Server.");			
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
			
	}
	
	public void deployTimer(){
		refreshTimer = new Timer();
		refreshTimer.scheduleAtFixedRate(new RefreshThread(),delay, refreshPeriod);
	}
	public void startImprovementTimer(){
		pathImprovementTimer = new Timer();
		pathImprovementTimer.scheduleAtFixedRate(new ImprovementThread(), delay, improvementPeriod);
	}
	
	public void contactRP(){
		// Method which sends request to Rendezvous Point for host location		
		rootNode = "192.168.47.1";
		rootNode = "192.168.0.11";
		System.out.println("Contaced RP root node address is: "  + rootNode);
		
	}
	/**
	 *  If UMRP set you as the rootNode, you don't have to query yourself to find child
	 *  and join, since you're already the root.
	 *  TODO: I guess it's all other client's responsibilities to remake the tree in case
	 *  you became root after the root left, meaning you don't know your children 
	 * 
	 * @throws UnknownHostException
	 */
	public void startJoinTree() throws UnknownHostException{
		// Contact Root node to start recursive process of finding suitable parent node
		
		if(!rootNode.equals(myAddress)){
			isJoining = true;
			System.out.println("Joining Algorithm started - Requesting child list from: " + rootNode);
			comm.sendDatagram(InetAddress.getByName(rootNode), REQUESTCHILDLIST);
		}
		else{
			System.out.println("I'm root node, no join required.");
			rootPath = new String[1];
			rootPath[0] = myAddress;
		}
	}
	/**
	 * Delete yourself from your RootPath and pop your grandparent as new potential parent.
	 * Wait a random delay before continuing the join algorithm until a new parent is found.
	 * The random delay is used to prevent all orphans from contacting the grandparent at the 
	 * same time.
	 * 
	 * @throws UnknownHostException
	 * @throws InterruptedException 
	 */
	public void startJoinTreeRepair() throws UnknownHostException, InterruptedException{
		isJoining = true;
		int rand = new Random(System.currentTimeMillis()).nextInt(randomIntRange);		
		Thread.sleep(rand);
		//new Timer().wait(rand);
		if(rootPath.length > 2){
			System.out.println("Joining Repair Algorithm started - Requesting child list from: " + rootPath[rootPath.length - 3]);		
			comm.sendDatagram(InetAddress.getByName(rootPath[rootPath.length - 3]), REQUESTCHILDLIST);
		}
		else{
			System.out.println("Joining Repair Algorithm started - Requesting child list from: " + rootNode);
			comm.sendDatagram(InetAddress.getByName(rootNode), REQUESTCHILDLIST);
		}
	}
	
	public void leaveGroup(){
		System.out.println("Leave Group message sent. ");
		// Method to leave group
	}
	
	// The following methods are currently invoked from the listening socket thread in 
	// SocketComm. Probably a much better way to do this....
	
	public void childListReceived(DatagramPacket pkt){
		System.out.println("Received Child List from: " + pkt.getAddress().getHostAddress());
		if(isJoining){			
			try {
				String data = new String(pkt.getData(),0,pkt.getLength(),"Cp1252");
				
				// Modify data string so that we remove the "CHILDLIST " portion and keep
				// only the relevant data
				System.out.println("String representation of data received: " + data);
				potentialParent = pkt.getAddress().getHostAddress();
				if(data.indexOf(" ") != -1){
					data = data.substring(data.indexOf(" "));
					// This node has to be the minRTT we've encountered, since if it were 
					// the previous parent, we wouldn't have requested a childlist from this
					// node to begin with.
					//minRTT = pingNode(pkt.getAddress().getHostAddress());				
					
					// We assume that the data was simply a list of IPv4 addresses
					// Separated by spaces.
					String[] children = data.split("[ ]+");
					String[] rttsToCheck = new String[children.length + 1];
					rttsToCheck[0] = pkt.getAddress().getHostAddress();
					int count = 1;
					for (String child : children){
						rttsToCheck[count] = child;
						count++;
					}
					rttClient.getRTT(rttsToCheck);
					int[][] results = rttClient.getResult();
					// assume that the 0th element has RTT, but can change to hold the average
					// of all 10 elements
					minRTT = results[0][0];
					count = 1;
					int rtt;
					for (String child : children){
						rtt = results[count][0];
						if(rtt < minRTT ){
							potentialParent = children[count - 1];
						}
					}
					// If the node we queried for it's child list was the closest node
					// to begin with, we'll stop looking at children lists and attempt to 
					// select queried node as parent
					if (potentialParent == pkt.getAddress().getHostAddress()){
						isJoining = false;
						System.out.println("Closest Node found. Requesting to join tree with parent set as: " + potentialParent);
						comm.sendDatagram(InetAddress.getByName(potentialParent), REQUESTJOIN);
					}
					// Else if one of the children is consider closer to us, request child list
					else{
						System.out.println("Contacting node child with MinRTT for Child List. Child: " + potentialParent);
						comm.sendDatagram(InetAddress.getByName(potentialParent), REQUESTCHILDLIST);
					}
				}
				else{
					System.out.println("Node returned no children. Requesting to join tree with parent set as: " + potentialParent);
					comm.sendDatagram(InetAddress.getByName(potentialParent), REQUESTJOIN);
				}
				
			} catch (UnsupportedEncodingException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (UnknownHostException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IOException e) {
				// TODO Auto-generated catch block
				System.err.println("Cannot ping RTTS. ");				
				//e.printStackTrace();
			}
		}
		
	}
	public void joinRequestReceived(DatagramPacket pkt){
		System.out.println("Join Request Received from: " + pkt.getAddress().getHostAddress());
		String IPaddr = pkt.getAddress().getHostAddress();
		if(!this.childNodeList.contains(IPaddr)){
			childNodeList.add(IPaddr);
		}
			// Reply with Ack so that client knows to update it's parent node value
			System.out.println("Join Request Approved, sending Ack to: " + pkt.getAddress().getHostAddress());
			comm.sendDatagram(pkt.getAddress(), ACKJOIN);
		
	}
	public void joinRequestAcknowledged(DatagramPacket pkt){
		String IPaddr = pkt.getAddress().getHostAddress();
		if(!this.parentNode.equals(IPaddr)){
			System.out.println("Join Ack Received from: " + pkt.getAddress().getHostAddress() + ". Set as parent.");
			parentNode = IPaddr;
		}
		// Set off Timer to issue out refreshMessages
		this.deployTimer();
		// Set off Timer to seek path improvement
		this.startImprovementTimer();
	}
	public void childRequestReceived(DatagramPacket pkt){
		if(!pkt.getAddress().getHostAddress().equals(myAddress)){
			System.out.println("Child List Request Received from: " + pkt.getAddress().getHostAddress());
			String childlist = "";
			for (String child : childNodeList){
				childlist += " " + child;
			}
			// The format of the data in this packet will be "CHILDLIST x.x.x.x y.y.y.y z.z.z.z ..."		
			comm.sendDatagram(pkt.getAddress(), CHILDLIST + childlist);
		}
	}

	public void leaveRequestReceived(DatagramPacket pkt){
		
		String IPaddr = pkt.getAddress().getHostAddress();
		// If the leaving node was a child, simply remove node from childNodeList
		if (childNodeList.contains(IPaddr)){
			System.out.println("Leave Request Received from child at: " + pkt.getAddress().getHostAddress() + ". Removing from child list.");
			
			childNodeList.remove(IPaddr);			
		}
		// If node was your parent, looks like you've been abandoned, start looking :(
		else if (IPaddr == parentNode){
			// According to Fig. 4, you trace back to your grandparent from your rootPath
			// and start the join procedure again
			String grandparent;
			if(rootPath.length > 2)
				grandparent = rootPath[rootPath.length - 3];
			else
				grandparent = rootNode;
			potentialParent = grandparent;
			try {
				System.out.println("Leave Request Received from parent at: " + pkt.getAddress().getHostAddress() + ". Restarting join algorithm.");
				comm.sendDatagram(InetAddress.getByName(potentialParent), REQUESTCHILDLIST);
			} catch (UnknownHostException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
	// Refreshes are sent from child to parent periodically
	public void refreshUpdateReceived(DatagramPacket pkt){
		
		String IPaddr = pkt.getAddress().getHostAddress();
		if(childNodeList.contains(IPaddr)){
			String rootpath = "";
			for (String path : rootPath){
				rootpath += " " + path;
			}
			System.out.println("Refresh packet received from child at: " + pkt.getAddress().getHostAddress() + ". Sending Notify path reply.");
			comm.sendDatagram(pkt.getAddress(),NOTIFYPATH + rootpath);
		}
	}
	// Path updates are sent from parent to child as a response to refreshes
	public void pathUpdateReceived(DatagramPacket pkt){
		String IPaddr = pkt.getAddress().getHostAddress();
		// Only deal with this if we got it from our parent, ignore otherwise
		if(IPaddr.equals(parentNode)){			
			try {
				System.out.println("path packet received from parent at: " + pkt.getAddress().getHostAddress() + ". Updating rootPath.");
				// Get data, ignore "PATH ", split values up, create new array and
				// finally add yourself to complete your root path
				String data = new String(pkt.getData(),0,pkt.getLength(),"Cp1252");
				data = data.substring(data.indexOf(" ") + 1);
				String[] newPath = data.split("[ ]+");
				String[] temp = new String[newPath.length + 1];
				for (int i = 0; i < newPath.length; i++){
					temp[i] = newPath[i];
				}
				temp[temp.length - 1] = myAddress;
				rootPath = temp;
				// Reset timerRefreshSent so that we don't think we haven't got any replies
				timerRefreshSent = 0;
			} catch (UnsupportedEncodingException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			
		}
	}

	public void datagramReceived(DatagramPacket pkt){
		System.out.println("Datagram packet received from: " + pkt.getAddress().getHostAddress());
	}
}

class SocketComm {
	private DatagramSocket socket;	
	private InetAddress localHost;
	private ListenThread listenSocket;
	private Bootstrap parentBoot;
	
	SocketComm(Bootstrap boot){
		// Open a datagram socket
		try {			
			localHost = InetAddress.getLocalHost();	
			parentBoot = boot;
			//socket = new DatagramSocket(5553, InetAddress.getByName(parentBoot.myAddress));	
			listenSocket = new ListenThread(this);
			listenSocket.start();
		} //catch (SocketException e) {
			// TODO Auto-generated catch block
			//e.printStackTrace();
		//} 
		catch (UnknownHostException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
			
	}

	// Send a UDP Datagram to specified destination
	public int sendDatagram(InetAddress dest, String data) {
		try {
			System.out.println("Socket bound to address: " + socket.getLocalAddress().getHostAddress());
			String datagram = data;
			byte[] buffer = datagram.getBytes("8859_1");
			DatagramPacket packet = new DatagramPacket(buffer,buffer.length,dest,4444);
			socket.send(packet);
		} catch (UnsupportedEncodingException e) {
			// For datagram.getBytes("8859_1");
			// TODO Auto-generated catch block
			e.printStackTrace();
			return -1;
		} catch (IOException e) {
			// For socket.send(packet);
			System.out.println("Sending error: " + e.getMessage());
			// TODO Auto-generated catch block
			e.printStackTrace();
			return -1;
		}
		return 0;
	}
	
	// Modified the ListenThread class to listen for incoming UDP packets and call a specific method depending on the type of 
	// packet received. Note, this will definitely have to change if we decide to use TCP for the control stuff.
	public class ListenThread extends Thread {
		private SocketComm parentComm;
		public ListenThread(SocketComm par) {
			super("ListenThread");
			parentComm = par;
		}


		public void run() {
			//DatagramSocket socket = null;
			try {
				socket = new DatagramSocket(4444, InetAddress.getByName(parentBoot.myAddress));			
			} catch (Exception e) {
				System.err.println("Could not listen on port: 4444.");
				System.exit(-1);
			}

			try {
				boolean listening = true;
				int count = 0;
				while(listening) {
					byte[] buf = new byte[256];
					System.out.println("Socket Comm waiting for packets (processed " + count + " already)... listening on: " + socket.getLocalAddress().toString());
					DatagramPacket packet = new DatagramPacket(buf, buf.length);
					socket.receive(packet);
					System.out.print("Packet " + count + ")received, time to process...");
					String packetString = new String(packet.getData(), "Cp1252");
					String packetSrcIP = new String(packet.getAddress().getHostAddress());
					if(packetString.startsWith("JOIN")){
						System.out.println("JOIN");
						this.parentComm.parentBoot.joinRequestReceived(packet);
					}
					else if(packetString.startsWith("ACKJOIN")){
						System.out.println("ACKJOIN");
						this.parentComm.parentBoot.joinRequestAcknowledged(packet);
					}
					else if (packetString.startsWith("CHILDLIST")){
						System.out.println("CHILDLIST");
						this.parentComm.parentBoot.childListReceived(packet);						
					}
					else if (packetString.startsWith("REQUESTCHILDREN")){
						System.out.println("REQUESTCHILDREN");
						this.parentComm.parentBoot.childRequestReceived(packet);						
					}
					else if (packetString.startsWith("LEAVE")){
						System.out.println("LEAVE");
						this.parentComm.parentBoot.leaveRequestReceived(packet);						
					}
					else if (packetString.startsWith("REFRESH")){
						System.out.println("REFRESH");
						this.parentComm.parentBoot.refreshUpdateReceived(packet);						
					}
					else if (packetString.startsWith("PATH")){
						System.out.println("PATH");
						this.parentComm.parentBoot.pathUpdateReceived(packet);						
					}
					else if (packetString.startsWith("DATA")){
						System.out.println("DATA");
						this.parentComm.parentBoot.datagramReceived(packet);						
					}
					count++;
					//String outputString = "You sent: " + new String(packet.getData(), "Cp1252" /* encoding */);
					//System.out.println(outputString);
					//InetAddress address = packet.getAddress();
		            //int port = packet.getPort();
		            //byte[] outBuf = outputString.getBytes("UTF8");
		            //packet = new DatagramPacket(outBuf, outBuf.length, address, port);
		            //socket.send(packet);
		            //System.out.println("Packet sent");
		            //count++;
				}
	            
	            socket.close();
	            System.out.println("Socket closed");
			} catch (Exception e) {
				System.out.println("SimpleServerThread Message: " + e.getMessage());
				e.printStackTrace();
			}
			
		}
	}
}
